<?php defined('SYSPATH') or die('No direct script access.');
/**
 * Base Context.
 */
class Jefri_Entity_Context_Core {
	/**
	 * @var  string  default context
	 */
	public static $default = 'default';

	/**
	 * @var  array  session instances
	 */
	protected static $instances = array();

	/**
	 * Creates a singleton session of the given type.
	 *
	 *     $context = Jefri_Entity_Context::instance();
	 *
	 */
	public static function instance($name = NULL) {
		if ($name === NULL)
		{	// Use the default type
			$name = static::$default;
		}

		if ( ! isset(static::$instances[$name]))
		{	// Load the configuration for this type
			$config = Kohana::config('context')->get($name);
			// Set the session class name
			$context = new Jefri_Entity_Context($config);
			// Create a new session instance
			static::$instances[$name] = $context;
		}

		return static::$instances[$name];
	}

	protected $context = NULL;

	public function __construct($settings) {
//Use this array to count the number of executions of methods.
$this->profile_methods = array();
$this->profile_time = array();
$this->profile_instance_id = UUID::v4();

		$this->context = json_decode(file_get_contents($settings['uri']));

		$q = count($this->context->entities);
		for($i=0; $i<$q; $i++)
		{	//Visit every element once
			$element = array_shift($this->context->entities);
			$this->context->entities[$element->name] = $element;

			$p = count($element->properties);
			for($j=0; $j<$p; $j++)
			{	//Visit every property once...
				$property = array_shift($element->properties);
				$element->properties[$property->name] = $property;
			}

			$p = count($element->relationships);
			for($j=0; $j<$p; $j++)
			{	//Visit every property once...
				$relationship = array_shift($element->relationships);
				$element->relationships[$relationship->name] = $relationship;
			}
		}
	}

	/**
	 * Return a definition of an entity.
	 */
	public function entity_definition($name) {
		if(!is_string($name))
		{
			$name = $name->_entity_type();
		}
		return Arr::get($this->context->entities, $name);
	}

	/**
	 * Find the relationship back to this guy...
	 *
	 * $type is the 
	 */
	public function back_rel($type, $relationship) {
		$def = $this->entity_definition($relationship->to->type);
		$back = null;
		foreach($def->relationships as $relation)
		{	// Looking through all the relationships
			if($relation->to->type == $type && $relation->name !== $relationship->name)
			{	//Found it
				$back = $relation;
			}
		}

		return $back;
	}

	/**
	 * Find an instance of an entity
	 *
	 * @param   string   Type
	 * @param   string   UUID
	 * @param   array    key->value value from which to match
	 */
	public function find($type, $uuid = NULL, $keypair = NULL) {
		$instances = Arr::get($this->interns, $type, array());
		if($uuid != NULL)
		{	//Return one lucky entity, or a null biotch.
			return Arr::get($instances, $uuid, NULL);
		}
		elseif($keypair != NULL)
		{	//Return instances of type matching key/pair
			$results = array();
			foreach($keypair as $key => $value)
			{
				foreach($instances as $instance)
				{
					if($instance->$key == $value)
						array_push($results, $instance);
				}
			}
			if(count($results) == 0)
				return NULL;

			return $results;
		}
		else
		{	//Return all
			return $instances;
		}
	}

	/**
	 * Get an array of this entity.
	 *
	 * @param   mixed    where clause describing returned entities.
	 * @param   array    pageination bounds
	 * @param   boolean  Whether to print debug info for the query.
	 *
	 * @return  mixed    array with one or all the entities.
	 */
	public function get($type, $where = NULL, &$page = NULL, $save = FALSE, $debug = FALSE) {
		$model = "Model_$type";
		$result = $this->get_empty($type, $where, $page, $debug);

		if (empty($result))
		{	//No results, so return a new object prototyped from the $where spec
			$result[0] = $this->intern(new $model($where));
			if($save)
			{	//Immediately save it, so as to verify persistance.
				$result[0]->save();
			}
		}

		return $result;
	}

	/**
	 * Get an array of these entities, possibly empty.
	 *
	 * @param   mixed    where clause describing returned entities.
	 * @param   array    pageination bounds
	 * @param   boolean  Whether to print debug info for the query.
	 *
	 * @return  mixed    array with one or all the entities.
	 */
	public function get_first($type, $where = NULL, $save = FALSE, $debug = FALSE) {
		$model = "Model_$type";
		$get = $model::get($where, $save, $debug);
		if(is_array($get))
		{	//Pull the first element out
			$get = array_shift($get);
		}
#		$get->save();//Immediately save it, so as to verify persistance.
#		             // This shouldn't be necessary... get will handle it.
		return $get;
	}

	/**
	 * Return a single instance of this entity, as described by the $where
	 * prototype. If the object is set in the DB, its UUID will filled.
	 * Otherwise, its properties will be filled from the $where prototype, but
	 * with a sentinel uuid.
	 * 
	 * @param   mixed    where clause describing the returned entity.
	 * @param   boolean  whether to print debug info for the query.
	 *
	 * @return  mixed    the prototyped entity.
	 */
	public function get_empty($type, $where = NULL, &$page = NULL, $save = FALSE, $debug = FALSE) {
		$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		//Reset shared variables. Hidden globals, otherwise, method calls would be too ugly.
		$this->models = array();
		$this->tables = array();

		//Start the query, using the named table.
		$model = 'Model_' . $type;

		$definition = $this->entity_definition($type);

		//If we only want one entity, and we know the key, then see if find has it!
		if(!empty($where))
		{
			$key_is_set = Arr::get($where, $definition->key, false);
#			if(is_array($where))
#			{
#				$key_is_set = isset($definition->key, $where);
#			}
#			else
#			{
#				$key_is_set = property_exists($where, $definition->key);
#			}
			if($key_is_set)
			{	//We are being specific
				$is_single = true;
				foreach($where as $w)
				{	//If we have an array, abort
					if(is_array($w))
						$is_single = true;
				}
				if($is_single)
				{	//We attempt to find the entity in memory and those other things, not because they are easy, but because they are hard.
					$find_result = $this->find($type, $where[$definition->key]);
					if($find_result != null)
						return array($find_result);
				}
			}
		}

#echo K::debug($type);
#echo K::debug($where);
#echo K::debug($definition);
#die();
		//We select from the table as normal, unless paginated.
		$subquery = $model::_table();

		if($page)
		{	//Subquery that will handle our pagination and sorting.
			$subquery = DB::select()->from($model::_table());
			$this->_page($model, $page, $subquery, $definition);
		}

		//The main query
		$query = DB::select()->from(array($subquery, $model::_table()));
		$this->_select($model, $definition, $query);

		if($page)
		{	//Subquery to get total record count && apply sorting to entire Query
			$total_query = DB::select(DB::expr('COUNT(`'.$model::_table().'`.`'.$model::_primary().'`)'))->from($model::_table());
			$query->select(array($total_query, "_total"));
		}

		//Apply the where clause, do some joins...etcetera.
		$this->_where($model, $where, $query, $definition); 

		if($page)
		{	//Finally, add the sorting if we have it.
			$this->_sort($model, $page, $query);
		}

		//TODO this should be in a logging class somewhere
Kohana::$log->add(Kohana::INFO, "Query :: " . $query->compile(Database::instance()));

#echo $query->compile(Database::instance());
#die();

		$result = $query->execute();
#Kohana::$log->add(Kohana::INFO, "Result Obj :: " . print_r($result, true));
		//Ready the return array.
		$results = $result->as_array();

#Kohana::$log->add(Kohana::INFO, "Result Array :: " . print_r($results, true));
#die();
		//If we paged, get the total of the entity type.
		if($page)
		{	//If there are results, we return the count, otherwise, 0
			if(count($results) > 0)
			{
				$page['total'] = $results[0]['_total'];
			}
			else
			{
				$page['total'] = 0;
			}
		}

		foreach($results as &$data)
		{	//Prototype an instance from the data.
			$data = $this->_expand($model, $data);
#			echo K::debug($this->interns);
#die;
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);

Kohana::$log->add(Kohana::INFO, "Profiling :: Returned Result Count: " . count($results));
Kohana::$log->add(Kohana::INFO, "Profiling :: " . print_r($this->profile_methods, true));
Kohana::$log->add(Kohana::INFO, "Profiling :: " . print_r($this->profile_time, true));
		return $results;
	}

	//These don't need to get passed around in every damned request. They should
	//get reset every time get_empty starts.
	private $models;
	private $tables;

	/**
	 * Add all the fields from the definition to the query.
	 */
	private function _select($model, $definition, $query) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
#Kohana::$log->add(Kohana::INFO, "_select $model");
		$props = $definition->properties;
		$table = $model::_table();
		$this->models[$model] = $table;
		$this->tables[$table] = $model;
		foreach($props as $prop)
		{	//Add all the particular model's properties
			$prop = $model::_lookup($prop->name);
			$query->select(array($prop, DB::expr("`{$prop}`")));
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	/**
	 * Prepare the appropriate rules from a $where array.
	 */
	private function _where($model, $where, $query, $definition) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
#Kohana::$log->add(Kohana::INFO, "_where " . Debug::obj($where, true));
		if(is_scalar($where))
		{	//A single scalar value is assumed to be a primary key.
			$query->where($model::_primary(), '=', $where, $definition);
		}
		else if( !empty($where))
		{	//Something keyed, either an object or array.
			$this->_where_array($model, $where, $query, $definition);
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	private function _where_array($model, $where, $query, $definition) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
#Kohana::$log->add(Kohana::INFO, "_where_array " . Debug::obj($where, true));
		$definition = Jefri_Entity_Context::instance()->entity_definition($model::_entity_type());
		foreach($where as $key => $value)
		{	//Each key is a condition
			if(($prop = Arr::get($definition->properties, $key, FALSE))!==FALSE)
			{	//This is a property...
				$key = $model::_lookup($key);
				$this->_condition($key, $value, 'where', $query, $definition);
			}
			elseif(($relationship = Arr::get($definition->relationships, $key, FALSE)) !== FALSE)
			{	//A relationship. Add it.
				$this->_where_nav($model, $relationship, $value, $query, $definition);
			}
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	/**
	 * Recursive function to add children.
	 */
	private function _where_nav($model, $relationship, $where, $query, $definition) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
#Kohana::$log->add(Kohana::INFO, "_where_nav " . Debug::obj($where, true));
		$to_model = 'Model_' . $relationship->to->type;
		$from_model = 'Model_' . $relationship->from->type;
		$to_def = $to_model::_entity_type();
		$to_def = Jefri_Entity_Context::instance()->entity_definition($to_def);

		$from_property = $relationship->from->property;

		$query->join($to_model::_table(), 'LEFT')->on(
			$from_model::_lookup($from_property),
			'=',
			$to_model::_lookup($relationship->to->property)
		);
		$this->_select($to_model, $to_def, $query);
		$this->_where_array($to_model, $where, $query, $definition);
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	/**
	 * Apply the key/value condition to the query.
	 */
	private function _condition($key, $value, $where, $query, $definition) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
#Kohana::$log->add(Kohana::INFO, "_condition " . Debug::obj($where, true));
		if(is_numeric($value))
		{	//Numbers must be equal
			$query->$where($key, '=', $value);
		}
		if(is_string($value))
		{	//Strings can be similar. Allows for '%value%' conditions.
			//However, strings can also be IDs. So, we need to check IDness
			#if(preg_match('/[a-zA-Z0-9]{8}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{4}\-[a-zA-Z0-9]{12}/', $value))
#			var_dump($definition);
			$model = 'Model_' . $definition->name;
			if($key === $model::_lookup($definition->key))
			{	//It's the ID, match exactly
				$query->$where($key, 'like', $value);
			}
			else
			{	//Just a field
				$query->$where($key, 'like', '%'.$value.'%');
			}
		}
		if(is_array($value))
		{	//Several options:
			if (
				($regexin = (isset($value["regexin"])))
				    OR
				($likein = (isset($value["likein"])))
			)
			{	//Extended "IN" syntax
				$terms = array_shift($value);
				foreach($terms as $term)
				{	//Add each term to the search critera
					$op = $regexin?"REGEX":($likein?"LIKE":"=");
					$term = $likein?"%{$term}%":$term;
					$query->or_where($key, $op, $term);
				}
			}
			else
			{	//Operator-based lookup
				$this->_condition_array($key, $value, $where, $query, $definition);
			}
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	private function _condition_array($key, $value, $where, $query, $definition) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
#Kohana::$log->add(Kohana::INFO, "_condition_array " . Debug::obj($where, true));
		$op = $value[0];
		if(Database_SQLite3::is_operator($op)) //This will be really bad...
		{	//First term is a valid SQL operator
			$v = $value[1];
			$query->$where($key, $op, $v);
		}
		elseif(is_array($op))
		{
			foreach($value as $val)
			{
				$this->_condition($key, $val, $where, $query, $definition);
			}
		}
		else
		{	//An array otherwise means 'one of these'.
			$query->$where($key, 'in', $value);
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	/**
	 * Apply page conditions to the query.
	 */
	private function _page($model, $page, $query) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		//Set pagination bounds.
		if( !empty($page['start']) )
		{	//Get the starting index...
			$query->offset($page['start']);
		}
		if(!empty($page['length']) && empty($page['limit']))
		{	//It makes sense to use either lentgh or limit. Using both is dumb.
			$page['limit'] = $page['length'];
		}
		if( !empty($page['limit']) )
		{	//Set the page li
			$query->limit($page['limit']);
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	private function _sort($model, $page, $query) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		if( !empty($page['order']) && is_array($page['order']))
		{	//Set ordering
			foreach($page['order'] as $key => $value)
			{	//Key will be Type.property, convert Type to table and concactenate
				$parts = explode('.', $key);
				$m = 'Model_' . $parts[0];
				$key = $m::_table().".".$parts[1];
				$query->order_by($key, $value);
			}
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
	}

	/**
	 * A private helper method to go from rows of SQL data to entities in
	 * memory. $data should be an associative array of one row from the returned
	 * SQL query, with keys as table.property and the values being the gotten
	 * values. This goes through the fields in the row, stores them in temp 
	 * prototype arrays, then goes through those temp arrays and expands/interns
	 * those to objects.
	 *
	 * $model	the model for the requested type
	 * $data	assoc array of the columns, keyed table.column 
	 * $this->tables	a lookup array of DBTable to Model
	 * $this->models	a lookup array of Model to DBTable
	 */
	private function _expand($model, $data) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		//$model is the core model we're expanding from. $data is an array of 
		//columns, keyed by name. The name is entity.property

		$type = $model::_entity_type();
		$entities = array();

		foreach($data as $prop => $val)
		{	//$prop is the dotted property name, $val is the value.
			if($prop != "_total")
			{	//Ignore the _total property.  It is special.
				//Put all the property values in temporary "prototype" arrays.
				$parts = explode('.', $prop);

				//TODO find a different way of resolving the key of an entity.
				$entity_key = $this->entity_definition(substr($this->tables[$parts[0]], 6))->key;
				if($parts[1] == $entity_key && $val == null)
				{	//This means our key is null, throw away the data.  We should NOT be building new entities here.
					unset($entities[$parts[0]]);
					break;
				}
				else
				{	//We don't have a null key, we are OK to keep around.
					$entity = isset($entities[$parts[0]]) ? $entities[$parts[0]] : array();
					$entity[$parts[1]] = $val;
					$entities[$parts[0]] = $entity;
				}
			}
		}

		$table = $this->models[$model];
		if(isset($entities[$table]))
		{	//Load the requested entity and any of its relationships for which there is data.
			$entity = $this->intern(new $model($entities[$table]));
			$entity->_new = false;
			unset($entities[$table]);

			$entities = $this->_expand_relationships($entity, $entities);
			if(count($entities > 0))
			{
				//TODO Should we try to expand remaining entities?
			}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
			return $entity;
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
		return NULL;
	}

	/**
	 * Recursively expands the relationships of an entity and any loaded from a query.
	 */
	private function _expand_relationships($entity, $entities){
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		$definition = $this->entity_definition($entity);
		$relationships = $definition->relationships;
		foreach($relationships as $rel)
		{
			$m = 'Model_'.$rel->to->type;
			if(isset($this->models[$m]) && isset($entities[$this->models[$m]]))
			{	//We have the data, let's set some relationships.
				$nav = $this->intern(new $m($entities[$this->models[$m]]));
				$prop = (strpos($rel->type, '_a') === FALSE)
					? "add_"
					: "set_";
				$prop .= $rel->name;
				$nav->_new = false;
				$entity->$prop($nav, FALSE);
				unset($entities[$this->models[$m]]);
				$entities = $this->_expand_relationships($nav, $entities);
			}
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
		return $entities;
	}

	/**
	 * Run a GET Aggregation. 
	 */
	public function aggregate($aggregate, $debug = false) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		//The return container
		$aggregation = array();
		//First, run the subquery...
		$subquery = $aggregate['_aggregate'];
		$subselect = array();
		unset($aggregate['_aggregate']);
		if(isset($subquery['_aggregate']))
		{	//Get the subselect from another aggregation
			$subselect = $this->aggregate($subquery);
		}
		else
		{	//Get the subselect from a selection
			$subselect = $this->get_empty($subquery);
		}
		$aggregation['_subselect'] = $subselect;

		//Now, we'll run the aggregates
		foreach($aggregate as $field => $func)
		{	//Need to apply each function to each field
			
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
		return $aggregation;
	}

	/**
	 * Update or insert the given entity.
	 *
	 * @param    mixed    Entity prototype to request, update, and persist.
	 */
	public function update($type, $data = NULL, $debug = false) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		$_model = "Model_$type";
		$_primary = $_model::_primary();

		$saved = FALSE;
		$primary = Arr::get($data, $_primary, -1);
		$model = $_model::get_empty($primary);//Primary key lookup, update that one
		if(count($model) > 0)
		{	//We got one, update it.
			unset($data[$_primary]);
			$model = $model[$primary];
			$model->_update($data);
			//TODO Should we do something here if we can't find it?
		}
		else
		{	//Build a new one with the data.
			unset($data[$primary]);
			$model = new $_model($data);
		}

		$saved = $model->save($debug);
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
		return array($saved, $model);
	}

	/** 
	 * Get a canonical instance of the entity in this context.
	 */
	private $interns = array();
	public function intern($entity, $updateOnIntern = true, $lookupOnMiss = false) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		if(is_array($entity))
		{	//Ha ha- method overloading, PHP!
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
			return $this->_intern_array($entity);
		}

		$type = Arr::get($this->interns, $entity->_entity_type(), array());

		// Look up any currently loaded entity.
		$intern = Arr::get($type, $entity->id(), null);

		if($intern === NULL && $lookupOnMiss)
		{	//Nothing currently interned, Load from the DB
			$intern = self::get_first($entity->_entity_type(), $entity->id());
		}

		if($intern === NULL)
		{	//Still haven't found anything!
			$intern = $entity;
		}

		if($updateOnIntern)
		{	//Update the stored values
			$definition = $entity->_definition();
			foreach($definition->properties as $property)
			{
				$name = $property->name;
				//TODO Take into account default values...
				$intern->$name = $entity->$name;
			}
		}

#		$type[$entity->id()] = $intern;
		$this->interns[$entity->_entity_type()][$entity->id()] = $intern;

$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);

		return $intern;
	}

	private function _intern_array($entities) {
$ct = Arr::get($this->profile_methods, __FUNCTION__, 0);$this->profile_methods[__FUNCTION__] = $ct + 1;$profile_start_time = microtime(true);
		if(!is_array($entities))
		{	//Oops, called the wrong one
			return $this->intern($ies);
		}

		$ents = array();
		foreach($entities as $entity)
		{
			$entity = $this->intern($entity);
			$ents[$entity->id()] = $entity;
		}
$this->profile_time[__FUNCTION__] = Arr::get($this->profile_time, __FUNCTION__, 0) + (microtime(true) - $profile_start_time);
		return $ents;
	}
}
